#!/usr/bin/python
#
# Copyright (c) 2008, Mathias Weber
# Copyright (c) 2008, Thomas Stauffer
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# $Id$

import sys
import math

def clamp(value, min, max):
    """
    @param value: The given value is clamped to the range: min <= value <= max.
    @param min: Accepted minimum.
    @param max: Accepted maximum.
    """
    if value < min:
        return min
    if value > max:
        return max
    return value

from direct.task import Task

import direct.directbase.DirectStart

base.setBackgroundColor(0.0, 0.0, 0.2)
base.disableMouse()
base.camLens.setNearFar(0.1, 100.0)
base.camLens.setFov(45.0)

woman = loader.loadModel("woman")
woman.reparentTo(render)
woman.setPos(-0.5, 0.0, 0.0)

man = loader.loadModel("man")
man.reparentTo(render)
man.setPos(0.5, 0.0, 0.0)

# Panda3D work with degrees, we do this as well. Just remember if you feed it to sin and cos.
cameraAlpha, cameraBeta, cameraRadius = -90.0, 60.0, 10.0
cameraX, cameraY, cameraZ = 0.0, 0.0, 0.0

# Track mouse position and mouse button state.
mouseX, mouseY, mouseButtonMiddle = 0, 0, False

def recalcCamera():
    """
    Angle Alpha is on the X/Y Plane, while angle beta also has influence on the
    Z axis.
    """

    global cameraAlpha, cameraBeta, cameraRadius

    # Limit camera angles.
    cameraBeta = clamp(cameraBeta, 10.0, 80.0)
    cameraRadius = clamp(cameraRadius, 5.0, 50.0)

    a = math.radians(cameraAlpha)
    b = math.radians(cameraBeta)
    r = cameraRadius

    x = r * math.cos(a) * math.cos(b)
    y = r * math.sin(a) * math.cos(b)
    z = r * math.sin(b)

    camera.setPos(cameraX + x, cameraY + y, cameraZ + z)
    camera.lookAt(cameraX, cameraY, cameraZ)

recalcCamera()

def panCamera(alpha, beta, radius):
    """
    Move the camera around the invisible point where the camera is looking at.
    """

    global cameraAlpha, cameraBeta, cameraRadius

    cameraAlpha += alpha
    cameraBeta += beta
    cameraRadius += radius

    recalcCamera()

def moveCamera(forward, laterally):
    """
    Move the camera and the invisible point where the camera is looking at.
    """

    global cameraX, cameraY, cameraZ

    angleForward = math.radians(cameraAlpha)
    angleLaterally = math.radians(cameraAlpha - 90.0)
    cameraX += forward * -math.cos(angleForward)
    cameraY += forward * -math.sin(angleForward)
    cameraX += laterally * -math.cos(angleLaterally)
    cameraY += laterally * -math.sin(angleLaterally)

    recalcCamera()

def taskMousePosition(task):
    """
    Track the mouse position to update the camera. Looks like Panda3D has no
    support for relative mouse movement therefore we do it on our own. Also
    looks like there is no possibility to get the current state of all mouse
    buttons, therefore we have rebuild this state information through the event
    system from Panda3D (this happens not here).

    This function depends on the frame rate. In a real word you should change
    the speed variable based on thei measured frame render time.
    """

    global mouseX, mouseY
    global cameraAlpha, cameraBeta, cameraRadius

    if base.mouseWatcherNode.hasMouse():
        absoluteX = base.win.getPointer(0).getX()
        absoluteY = base.win.getPointer(0).getY()

        relativeX = absoluteX - mouseX
        relativeY = absoluteY - mouseY

        speed = 1.0

        if mouseButtonMiddle and ((relativeX != 0) or (relativeY != 0)):
            cameraAlpha += float(relativeX) * speed
            cameraBeta += float(relativeY) * speed
            recalcCamera()

        mouseX, mouseY = absoluteX, absoluteY

    return task.cont

taskMgr.add(taskMousePosition, "MousePosition")

def mouseClick(button):
    global mouseButtonMiddle
    mouseButtonMiddle = button

base.accept("escape", sys.exit)
base.accept("a", render.analyze)
base.accept("o", base.oobe)

base.accept("6", panCamera, [10.0, 0.0, 0.0])
base.accept("6-repeat", panCamera, [10.0, 0.0, 0.0])
base.accept("4", panCamera, [-10.0, 0.0, 0.0])
base.accept("4-repeat", panCamera, [-10.0, 0.0, 0.0])
base.accept("8", panCamera, [0.0, 10.0, 0.0])
base.accept("8-repeat", panCamera, [0.0, 10.0, 0.0])
base.accept("2", panCamera, [0.0, -10.0, 0.0])
base.accept("2-repeat", panCamera, [0.0, -10.0, 0.0])
base.accept("9", panCamera, [0.0, 0.0, 1.0])
base.accept("9-repeat", panCamera, [0.0, 0.0, 1.0])
base.accept("3", panCamera, [0.0, 0.0, -1.0])
base.accept("3-repeat", panCamera, [0.0, 0.0, -1.0])
base.accept("l", moveCamera, [0.0, 1.0])
base.accept("j", moveCamera, [0.0, -1.0])
base.accept("i", moveCamera, [1.0, 0.0])
base.accept("k", moveCamera, [-1.0, 0.0])

base.accept("wheel_up", panCamera, [0.0, 0.0, -1.0])
base.accept("wheel_down", panCamera, [0.0, 0.0, 1.0])
base.accept("mouse2", mouseClick, [True])
base.accept("mouse2-up", mouseClick, [False])

run()
